/*******************************************************************************
 * Copyright (c) 2000, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Andreas Schmid, service@aaschmid.de - Locate test method even if it contains parameters - http://bugs.eclipse.org/343935
 *******************************************************************************/
package org.eclipse.jdt.internal.junit.ui;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;

import org.eclipse.jface.dialogs.MessageDialog;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;

import org.eclipse.ui.PlatformUI;

import org.eclipse.ui.texteditor.ITextEditor;

import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaConventions;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;

import org.eclipse.jdt.internal.corext.util.JavaConventionsUtil;
import org.eclipse.jdt.internal.junit.BasicElementLabels;
import org.eclipse.jdt.internal.junit.Messages;
import org.eclipse.jdt.internal.junit.model.TestCaseElement;
import org.eclipse.jdt.internal.junit.model.TestElement;

import org.eclipse.jdt.internal.ui.actions.SelectionConverter;

/**
 * Open a class on a Test method.
 */
public class OpenTestAction extends OpenEditorAction {

	private String fMethodName;
	private String[] fMethodParamTypes;
	private IMethod fMethod;
	private int fLineNumber= -1;

	private IType fType;

	public OpenTestAction(TestRunnerViewPart testRunnerPart, TestCaseElement testCase, String[] methodParamTypes) {
		this(testRunnerPart, testCase.getClassName(), extractRealMethodName(testCase), methodParamTypes, true);
		String trace= testCase.getTrace();
		if (trace != null) {
			String rawClassName= TestElement.extractRawClassName(testCase.getTestName());
			rawClassName= rawClassName.replaceAll("\\.", "\\\\."); //$NON-NLS-1$//$NON-NLS-2$
			rawClassName= rawClassName.replaceAll("\\$", "\\\\\\$"); //$NON-NLS-1$//$NON-NLS-2$
			Pattern pattern= Pattern.compile(FailureTrace.FRAME_PREFIX
					+ rawClassName + '.' + fMethodName
					+ "\\(.*:(\\d+)\\)"  //$NON-NLS-1$
			);
			Matcher matcher= pattern.matcher(trace);
			if (matcher.find()) {
				try {
					fLineNumber= Integer.parseInt(matcher.group(1));
				} catch (NumberFormatException e) {
					// continue
				}
			}
		}
	}

	public OpenTestAction(TestRunnerViewPart testRunner, String className) {
		this(testRunner, className, null, null, true);
	}

	public OpenTestAction(TestRunnerViewPart testRunner, String className, String method, String[] methodParamTypes, boolean activate) {
		super(testRunner, className, activate);
		PlatformUI.getWorkbench().getHelpSystem().setHelp(this, IJUnitHelpContextIds.OPENTEST_ACTION);
		fMethodName= method;
		fMethodParamTypes= methodParamTypes;
	}

	private static String extractRealMethodName(TestCaseElement testCase) {
		//workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=334864 :
		if (testCase.isIgnored() && JavaConventions.validateJavaTypeName(testCase.getTestName(), JavaCore.VERSION_1_5, JavaCore.VERSION_1_5, null).getSeverity() != IStatus.ERROR) {
			return null;
		}

		//workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=275308 :
		String testMethodName= testCase.getTestMethodName();
		for (int i= 0; i < testMethodName.length(); i++) {
			if (!Character.isJavaIdentifierPart(testMethodName.charAt(i))) {
				return testMethodName.substring(0, i);
			}
		}
		return testMethodName;
	}

	@Override
	protected IJavaElement findElement(IJavaProject project, String className) throws JavaModelException {
		IType type= findType(project, className);
		if (type == null)
			return null;

		if (fMethodName == null) {
			fType= type;
			return type;
		}

		IMethod method= null;
		try {
			method= findMethod(type);
			if (method == null) {
				ITypeHierarchy typeHierarchy= type.newSupertypeHierarchy(null);
				IType[] supertypes= typeHierarchy.getAllSupertypes(type);
				for (IType supertype : supertypes) {
					method= findMethod(supertype);
					if (method != null)
						break;
				}
			}
		} catch (OperationCanceledException e) {
			// user cancelled the selection dialog - ignore and proceed
		}
		if (method == null) {
			if (fLineNumber < 0) {
				String title= JUnitMessages.OpenTestAction_dialog_title;
				String message= Messages.format(JUnitMessages.OpenTestAction_error_methodNoFound, BasicElementLabels.getJavaElementName(fMethodName));
				MessageDialog.openInformation(getShell(), title, message);
			}
			return type;
		}

		fMethod= method;
		return method;
	}

	private IMethod findMethod(IType type) {
		IStatus status= JavaConventionsUtil.validateMethodName(fMethodName, type);
		if (! status.isOK())
			return null;

		List<IMethod> foundMethods= new ArrayList<>();
		try {
			PlatformUI.getWorkbench().getProgressService().busyCursorWhile(monitor -> {
				StringBuilder methodPattern= new StringBuilder(type.getFullyQualifiedName('.')).append('.').append(fMethodName);
				if (fMethodParamTypes != null && fMethodParamTypes.length > 0) {
					String paramTypes= Arrays.stream(fMethodParamTypes).map(paramType -> {
						try {
							return paramType= Signature.toString(paramType);
						} catch (IllegalArgumentException e1) {
							// return the paramType as it is
						}
						return paramType.replace('$', '.'); // for nested classes... See OpenEditorAction#findType also.
					}).collect(Collectors.joining(", ", "(", ")")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					methodPattern.append(paramTypes);
				} else {
					methodPattern.append("()"); //$NON-NLS-1$
				}
				int matchRule= SearchPattern.R_ERASURE_MATCH | SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE;
				SearchPattern searchPattern= SearchPattern.createPattern(methodPattern.toString(), IJavaSearchConstants.METHOD, IJavaSearchConstants.DECLARATIONS, matchRule);
				if (searchPattern == null) {
					return;
				}
				SearchRequestor requestor= new SearchRequestor() {
					@Override
					public void acceptSearchMatch(SearchMatch match) throws CoreException {
						Object element= match.getElement();
						if (element instanceof IMethod) {
							foundMethods.add((IMethod) element);
						}
					}
				};
				SearchParticipant[] participants= new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() };
				IJavaSearchScope scope= SearchEngine.createJavaSearchScope(new IJavaElement[] { type });
				try {
					new SearchEngine().search(searchPattern, participants, scope, requestor, monitor);
				} catch (CoreException e2) {
					JUnitPlugin.log(e2);
				}
			});
		} catch (InvocationTargetException e) {
			JUnitPlugin.log(e);
		} catch (InterruptedException e) {
			// user cancelled
		}

		if (foundMethods.size() == 1) {
			return foundMethods.get(0);
		} else if (foundMethods.size() > 1) {
			IMethod method= openSelectionDialog(foundMethods);
			if (method == null) {
				throw new OperationCanceledException();
			}
			return method;
		}

		// search just by name and number of parameters, if method not found yet
		try {
			for (IMethod method : type.getMethods()) {
				String methodName= method.getElementName();
				if (fMethodName.equals(methodName)) {
					int numOfParams= method.getNumberOfParameters();
					int requiredNumOfParams= 0;
					if (fMethodParamTypes != null) {
						requiredNumOfParams= fMethodParamTypes.length;
					}
					if (numOfParams == requiredNumOfParams) {
						foundMethods.add(method);
					}
				}
			}
			if (foundMethods.isEmpty()) {
				return null;
			} else if (foundMethods.size() > 1) {
				IMethod method= openSelectionDialog(foundMethods);
				if (method == null) {
					throw new OperationCanceledException();
				}
				return method;
			} else {
				return foundMethods.get(0);
			}
		} catch (JavaModelException e) {
			// if type does not exist or if an exception occurs while accessing its resource => ignore (no method found)
		}

		return null;
	}

	private IMethod openSelectionDialog(List<IMethod> foundMethods) {
		IMethod[] elements= foundMethods.toArray(new IMethod[foundMethods.size()]);
		String title= JUnitMessages.OpenTestAction_dialog_title;
		String message= JUnitMessages.OpenTestAction_select_element;
		return (IMethod) SelectionConverter.selectJavaElement(elements, getShell(), title, message);
	}

	@Override
	protected void reveal(ITextEditor textEditor) {
		if (fLineNumber >= 0) {
			try {
				IDocument document= textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput());
				int lineOffset= document.getLineOffset(fLineNumber-1);
				int lineLength= document.getLineLength(fLineNumber-1);
				if (fMethod != null) {
					try {
						ISourceRange sr= fMethod.getSourceRange();
						if (sr == null || sr.getOffset() == -1
								|| lineOffset < sr.getOffset()
								|| sr.getOffset() + sr.getLength() < lineOffset + lineLength) {
							throw new BadLocationException();
						}
					} catch (JavaModelException e) {
						// not a problem
					}
				}
				textEditor.selectAndReveal(lineOffset, lineLength);
				return;
			} catch (BadLocationException x) {
				// marker refers to invalid text position -> do nothing
			}
		}
		if (fMethod != null) {
			try {
				ISourceRange range= fMethod.getNameRange();
				if (range != null && range.getOffset() >= 0)
					textEditor.selectAndReveal(range.getOffset(), range.getLength());
				return;
			} catch (JavaModelException e) {
				// not a problem
			}
		}
		if (fType != null) {
			try {
				ISourceRange range= fType.getNameRange();
				if (range != null && range.getOffset() >= 0)
					textEditor.selectAndReveal(range.getOffset(), range.getLength());
			} catch (JavaModelException e) {
				// not a problem
			}
		}
	}

}
